IFT6758 Blog A-09 Data science on NHL data

Milestone 1

Section 1 : Acquisition de données

Question 1.1

Rédigez un bref tutoriel sur comment votre équipe a téléchargé l'ensemble de données. Imaginez que vous cherchiez un guide sur comment télécharger les données play-by-play; votre guide devrait vous faire dire "Parfait - c'est exactement ce que je cherchais!". Incluez votre fonction/classe et fournissez un exemple de son utilisation.

Les données que nous désirons acquérir sont disponibles à partir d’un API utilisant des url telles que :

https://statsapi.web.nhl.com/api/v1/game/2017020001/feed/live/

2017020001 correspond au game_id de la partie. Le game_id est composé de trois sections, soit l’année de la saison (2017), le type de saison (02) et le numéro de la partie (0001). Les types de saison nous intéressant sont 02 pour les saisons régulières. Pour obtenir l’entièreté de la saison régulière de 2016, on peut simplement itérer sur le numéro de la partie en utilisant :

access_point = "https://statsapi.web.nhl.com/api/v1/game/"
season = 2016
game_id = 1
game_id_str = f"{season}02{str(game_id).zfill(4)}"
url = f"{access_point}{game_id_str}/feed/live/"

et augmenter game_id. On peut alors définir le nom du fichier où les données seront sauvegardées afin de vérifier s’il existe déjà localement et ne pas le télécharger.

save_directory = "../NHLGameData"
filename = os.path.join(save_directory, f"game_{game_id_str}.json")

if os.path.exists(filename):
    #do not download filename and continue to next file
    game_id += 1
    continue

Dans le cas ᴏù nous voulons télécharger le fichier, il nous faudra commencer par vérifier si celui-ci existe dans l’API.

response = requests.get(url)
if response.status_code == 200:
    # download file
elif response.status_code == 404:
    # Game not found, exit the loop
    break
else:
    print(f"Error in regular match ID {game_id_str}: {response.status_code}")

Si la partie existe et est accessible, le code renvoyé sera 200. Si la partie n’existe pas (erreur 404), cela veut dire que le game_id présent est devenu trop haut et il est nécessaire de quitter la boucle d’itération de match afin de ne pas faire des demandes inutiles à l’API. Si un autre code est renvoyé, cela n’est pas normal et il est intéressant de le notifier dans l’interface. Finalement, si on veut télécharger le fichier, il est nécessaire de savoir que l’API enregistre ses données en format json. On peut donc accéder aux données pertinentes et les téléchargées dans un fichier.

playByPlay = response.json()['liveData']['plays']['allPlays']
home_team = responses.json()['gameData']['teams']['home']['name']
away_team = response.json()['gameData']['teams']['away']['name']
periods = response.json()['liveData']['linescore']['periods']

# create and save the json informations into the file
with open(filename, "w") as file:
    data_to_save = {"playByPlay": playByPlay,"periods": periods, "home": home_team, "away": away_team}
    json.dump(data_to_save, file)

Section 2 : Débogage interactif

Question 2.1

Implémentez un ipywidget qui vous permet de parcourir tous les événements, pour chaque match d'une saison donnée, avec la possibilité de changer entre la saison régulière et les séries éliminatoires. Dessinez les coordonnées de l'événement sur l'image de la patinoire fournie, similaire à l'exemple ci-dessous (vous pouvez simplement imprimer les données de l'événement lorsqu'il n'y a pas de coordonnées). Vous pouvez également imprimer toutes les informations que vous jugez utiles, telles que les métadonnées du jeu/boxscores et les résumés des événements (mais ce n'est pas obligatoire). Prenez une capture d'écran de l'outil et ajoutez-la à l’article de blog, accompagnée du code de l'outil et d'une brève description (1-2 phrases) de ce que fait votre outil. Vous n'avez pas à vous soucier de l'intégration de l'outil dans le blogpost.

q2_ipywidgets

L’outil développé permet de visualiser les différents évènements assez facilement à l’aide de ipywidget tout en naviguant les différentes parties, années et type de saisons. Sur une erreur de la visualisation, par exemple sur un début de partie ou n’importe quel évènement où les coordonnées ne sont pas disponibles, le texte en format json est montré à la place. L’outil balaye les fichiers locaux disponibles localement pour faire la liste des données à visualiser.

q2_no-event

import ipywidgets as widgets
from IPython.display import display
import matplotlib.image as mpimg
import json
import os
import matplotlib.pyplot as plt

class event_visualizer(object):
    def __init__(self, path, img_path):
        self.path = path
        self.rink_img = mpimg.imread(img_path)
        self.initialize_values()
        self.create_widgets()

    def initialize_values(self):
            self.season_type = '02' # init only
            self.games_list = self.get_games_list()
            self.year_list = self.get_year_list()
            self.year = self.year_list[0] # init only
            self.game_ids = self.get_games_in_season()
            self.game_id = self.game_ids[0] # init only
            self.file = self.read_file()
            self.events = self.file['playByPlay']
            self.event_id = 0 # init only

    def create_widgets(self):
        self.w_season_type = widgets.ToggleButtons(
            options=[('Regular', '02'), ('Playoff', '03')],
            description='Season type'
            )
        self.w_season_year = widgets.ToggleButtons(
            options=self.year_list,
            description="Season year"
            )
        self.w_game_ids = widgets.SelectionSlider(
            options=self.game_ids,
            value=self.game_ids[0],
            description="Game id"
            )

        self.w_season_year.observe(self.update, 'value')
        self.w_season_type.observe(self.update, 'value')
        self.w_game_ids.observe(self.update, 'value')

        self.grid = widgets.GridspecLayout(3,1)
        self.grid[0,0] = self.w_season_type
        self.grid[1,0] = self.w_season_year
        self.grid[2,0] = self.w_game_ids

    def update(self, *args):
        self.season_type = self.w_season_type.value
        self.games_list = self.get_games_list()

        self.year_list = self.get_year_list()
        self.w_season_year.options = self.year_list
        self.year = self.w_season_year.value

        self.game_ids = self.get_games_in_season()
        self.w_game_ids.options = self.game_ids
        self.game_id = self.w_game_ids.value

        self.file = self.read_file()
        self.events = self.file['playByPlay']

    def get_games_list(self):
        games_list = os.listdir(self.path)
        return [ i[5:9] + "_" + i[11:15] for i in games_list if i[9:11]==self.season_type]

    def get_year_list(self):
        year_list = list(set( [i.split("_")[0] for i in self.games_list] ))
        year_list.sort()
        return year_list

    def get_games_in_season(self):
        season_games = [ i.split("_")[1] for i in self.games_list if i[0:4]==self.year ]
        season_games.sort() #should be by default but still safer
        return season_games

    def read_file(self):
        f = open(self.path + "game_{year}{season}{id}.json".format(year=self.year,
                                                                   season=self.season_type,
                                                                   id=self.game_id)
                )
        data = json.load(f)
        return data

    def plot_event(self, event_id):
        try :
            x = self.events[event_id]['coordinates']['x']
            y = self.events[event_id]['coordinates']['y']

            title = self.events[event_id]['result']['description']
            time = self.events[event_id]['about']['periodTime']
            period = self.events[event_id]['about']['period']

            fig = plt.figure()
            ax = fig.add_axes([0,0,1,1])
            ax.imshow(self.rink_img, extent=[100, -100, 42.5, -42.5])
            ax.set_xlim(-100, 100)
            ax.set_xlabel("feet")
            ax.set_ylim(-42.5, 42.5)
            ax.set_ylabel("feet")
            ax.set_title("{description}\n{time} P-{period}\n".format(description=title,
                                                                   time=time,
                                                                   period=period)
            )
            ax.plot(x,y, 'ok')

            plt.figtext(0.15, 0.8, self.file['home']) # positions for these are hard coded since they shouldn't change
            plt.figtext(0.65, 0.8, self.file['away'])

            plt.show()
        except:
            print( json.dumps(self.events[event_id], indent=4) )


game_viz = event_visualizer("data/", "nhl_rink.png")
display(game_viz.grid)

widgets.interact(game_viz.plot_event, event_id=(0, len(game_viz.events)-1))

Section 4 : Nettoyer les données

Question 4.1

Dans votre article de blog, incluez un petit extrait de votre dataframe final (par exemple, en utilisant head(10)). Vous pouvez simplement inclure une capture d'écran plutôt que de vous battre pour que les tableaux soient soigneusement formatés en HTML/markdown.
df.sample(10)

q4-1

Question 4.2

Vous remarquerez que le champ de « force » (c.-à-d. égal, avantage numérique ou en désavantage numérique) n'existe que pour les buts, pas pour les tirs. De plus, il n'inclut pas la force réelle des joueurs sur la glace (c'est-à-dire 5 contre 4, ou 5 contre 3, etc.). Discutez de la façon dont vous pourriez ajouter les informations sur la force réelle (c'est-à-dire 5 contre 4, etc.) aux tirs et aux buts, compte tenu des autres types d'événements (autre que ces derniers) et des autres données disponibles. Vous n'avez pas besoin d’implémenter cette fonctionnalité pour ce milestone.

Dans un match de hockey, chaque action sur la glace a son importance, qu’il s’agisse d’un tir, d’un but ou même d’une pénalité. Lorsque nous examinons les statistiques, nous pouvons souvent voir le nombre de tirs ou de buts, mais une dimension souvent négligée est la “force réelle” lors de ces actions. La force réelle se réfère à la situation numérique sur la glace, comme 5 contre 4 lors d’une pénalité ou 5 contre 5 en jeu égal.

Pour déterminer cette force réelle, il est essentiel d’intégrer les données des événements de pénalité dans notre DataFrame. Chaque pénalité indique la durée pendant laquelle une équipe jouera en infériorité numérique. En suivant ces pénalités et en les associant aux tirs et aux buts qui se produisent pendant leur durée, nous pouvons déduire la force réelle de chaque attaque. Par exemple, si une pénalité de deux minutes est infligée à 10:00 et qu’un tir est enregistré à 10:30, on peut conclure que ce tir a eu lieu en avantage numérique.

Mais attention, d’autres événements peuvent influencer la situation. Un but marqué pendant un avantage numérique pourrait annuler la pénalité, ou des pénalités simultanées pourraient maintenir un jeu à 5 contre 5. Il est donc crucial de prendre en compte tous les événements du jeu.

Question 4.3

En quelques phrases, discutez d’au moins 3 caractéristiques supplémentaires que vous pourriez envisager de créer à partir des données disponibles dans cet ensemble de données. Nous ne cherchons pas de réponses particulières, mais si vous avez besoin d'inspiration, un tir ou un but pourrait-il être classé comme un rebond/tir en contre-attaque (expliquez comment identifier ceux-ci)?

Efficacité des Joueurs

En utilisant les données sur les joueurs qui tirent, on pourrait créer une métrique pour évaluer l’efficacité des tirs de chaque joueur. Cela consisterait à calculer le pourcentage de tirs d’un joueur qui se transforment en buts. Cette métrique pourrait aider à identifier les joueurs les plus dangereux ou les plus efficaces en face du but.

Efficacité du Gardien de but

En se basant sur le nombre de tirs qu’un gardien de but reçoit et le nombre de buts qu’il concède, on pourrait calculer un taux d’arrêt pour chaque gardien. Cela donnerait une mesure directe de l’efficacité d’un gardien à stopper les tirs adverses.

Tirs en Contre-Attaque - Rebonds

Pour identifier un tir en contre-attaque, on pourrait examiner la séquence des événements. Si un “Hit” ou un “Turnover” est suivi rapidement par un tir de l’équipe opposée, cela pourrait indiquer un tir en contre-attaque. Si un tir est suivi immédiatement par un autre tir de la même équipe sans qu’un événement significatif se produise entre les deux (comme un dégagement ou un arrêt de jeu), le second tir pourrait être considéré comme un rebond.

Section 5 : Visualisations simples

Question 5.1

Produisez un graphique comparant les types de tirs de toutes les équipes dans une saison de votre choix (i.e. agrégez juste sur tous les tirs). Superposez le nombre de buts sur le nombre de tirs. Quel semble être le type de tir le plus dangereux? Le type de tir le plus courant? Pourquoi est-ce que vous avez choisi ce type de graphique? Ajoutez ce graphique et cette discussion à votre article de blog.

s<html>

</html>

Le graphique ci-dessus présente une comparaison des différents types de tirs par rapport aux buts marqués pour chaque type lors de la saison 2019-20.

Type de tir le plus courant

On peut constater que “Wrist Shot” est clairement le type de tir le plus fréquemment utilisé par les équipes.

Type de tir le plus dangereux

En observant la proportion de buts par rapport au nombre total de tirs, le “Tip-In” semble être le plus efficace pour marquer des buts. Cependant, le “Wrist Shot”, en raison de sa fréquence élevée, a également produit un grand nombre de buts.

Pourquoi ce type de graphique

On a choisi un graphique à barres empilées car il permet de comparer directement le nombre total de tirs et le nombre de buts pour chaque type de tir. Les couleurs Rouges et Vertes permettent de distinguer les buts des tirs.

Question 5.2

Quelle est la relation entre la distance à laquelle un tir a été effectué et la chance qu'il s'agisse d'un but? Produisez un graphique pour chaque saison entre 2018-19 et 2020-21 pour répondre à cette question, et ajoutez-le à votre article de blog avec quelques phrases décrivant le graphique. Y a-t-il eu beaucoup de changements au cours des trois dernières saisons? Pourquoi est-ce que vous avez choisi ce type de graphique? a. Trouver la distance du tir nécessite la combinaison de plusieurs des données, c’est à vous de trouver une méthode qui fonctionne pour la majorité des parties (certaines ont des informations manquantes). b. Si vous notez quelques données aberrantes, ne vous inquiétez pas, on en reparlera dans le milestone 2.

Comprendre la relation entre la distance du tir et la probabilité de marquer est crucial pour les équipes et les entraîneurs. Cette connaissance peut influencer les stratégies offensives des équipes par exemple. Il est donc toujours utile de visualiser ces relations. Les graphiques ci-dessous montrent la probabilité de marquer un but en fonction de la distance du tir pour les saisons 2018-19, 2019-20 et 2020-21.

q5-2_2018 q5-2_2019 q5-2_2020

Observations

Au cours des saisons 2018 et 2019, les données montrent clairement que les tirs à une distance plus courte ont une probabilité significativement plus élevée de se transformer en buts. Cependant, la saison 2020-21 présente une tendance différente, avec des probabilités de marquer qui ne sont pas aussi élevées, même pour les tirs à courte distance. Cette différence est attribuée au nombre de matches joués cette saison en raison de la pandémie de COVID-19 À mesure que la distance augmente, la probabilité de marquer diminue. Cependant, il y a certaines distances où il y a des pics ou des augmentations notables de la probabilité. Cela pourrait être dû à des tirs précis, comme les « Slap shots », qui sont puissants et peuvent surprendre le gardien. Il y a une certaine cohérence dans la tendance à travers les trois saisons, bien que de légères variations puissent être observées.

Pourquoi ce type de graphique

On a choisi un graphique linéaire car il permet de voir clairement comment une variable (dans ce cas, la probabilité de marquer) change en fonction d’une autre variable continue (dans ce cas, la distance du tir). Les graphiques linéaires sont un excellent choix pour notre situation puisqu’ils captent les pics et les creux.

Question 5.3

Combinez les informations des sections précédentes pour produire un graphique qui montre le pourcentage de buts (# buts / # tirs) en fonction à la fois de la distance par rapport au filet et de la catégorie de types de tirs (vous pouvez choisir une seule saison de votre choix). Discutez brièvement de vos conclusions. Par exemple, quels sont les types de tirs les plus dangereux?

Tirs rapprochés

Comme attendu, il y a une concentration plus élevée (zones plus claires) de buts pour les tirs effectués à courte distance, en particulier pour les “Tip-Ins” et les “Snap Shots”. Cela renforce l’idée que les tirs rapprochés ont généralement une probabilité plus élevée de se transformer en des buts.

Efficacité des “Tip-Ins”

Les “Tip-Ins”, semblent être particulièrement efficaces à courte distance. Cela pourrait être dû à la nature imprévisible de ces tirs, rendant difficile pour le gardien de but de les arrêter.

Variabilité avec la distance

Pour les tirs tels que les “Slap Shots” et les “Wrist Shots”, il y a une variabilité notable dans le pourcentage de buts à mesure que la distance augmente. Bien que ces tirs soient généralement moins efficaces à longue distance, il y a des zones où ils ont un taux de réussite relativement élevé, peut-être en raison de la puissance ou de la précision du tir.

Zones plus foncées à longue distance

Comme prévu, la plupart des types de tirs ont un pourcentage de buts plus faible lorsqu’ils sont pris à longue distance. Cela est logique car il est généralement plus difficile de marquer depuis une distance éloignée. Sauf que pour quelques cas, on peut remarquer des grands pourcentages et c’est a cause du nombre limités des tirs effectués à ces distances durant cette saison.

Section 6 : Visualisations avancées

Question 6.1

Exportez les 4 graphiques de zone offensive au format HTML et intégrez-les dans votre article de blog. Votre graphique doit permettre aux utilisateurs de sélectionner n'importe quelle équipe pour la saison sélectionnée. Note: Parce que vous pouvez trouver ces graphiques sur l’internet, répondre à ces questions sans produire ces graphiques ne vous rapportera pas de points !

…content…

Question 6.2

Discutez (en quelques phrases) de ce que vous pouvez interpréter à partir de ces graphiques.

…content…

Question 6.3

Considérez l'Avalanche du Colorado; jetez un œil à leur carte de tir au cours de la saison 2016-17. Discutez de ce que vous pourriez dire sur l'équipe au cours de cette saison. Regardez maintenant la carte de tirs de l'Avalanche du Colorado pour la saison 2020-21 et discutez de ce que vous pouvez conclure de ces différences. Est-ce que ça a du sens? Astuce : regardez le classement.

…content…

Question 6.4

Considérez les Sabres de Buffalo, une équipe qui a connu des difficultés ces dernières années, et comparez-les au Lightning de Tampa Bay, une équipe qui a remporté la coupe Stanley pour deux années consécutives. Regardez les plans de tir de ces deux équipes des saisons 2018-19, 2019-20 et 2020-21. Discutez des observations que vous pouvez faire. Y a-t-il quelque chose qui pourrait expliquer le succès du Lightning, ou les problème des Sabres ? Est-ce que ces images sont suffisantes pour tout comprendre les succès ou problèmes d’une équipe?

…content…